Skip to content

Add Rust SDK (technical preview)#1164

Merged
stephentoub merged 87 commits intomainfrom
tclem/rust-sdk-release-prep
May 6, 2026
Merged

Add Rust SDK (technical preview)#1164
stephentoub merged 87 commits intomainfrom
tclem/rust-sdk-release-prep

Conversation

@tclem
Copy link
Copy Markdown
Member

@tclem tclem commented Apr 29, 2026

Adds a Rust SDK alongside the existing Node, Python, Go, and .NET SDKs in this repo. Same JSON-RPC client model, same protocol, same session lifecycle — just in Rust.

Important

Technical preview. This is published as github-copilot-sdk = "0.1" (pre-1.0) and the public API is subject to breaking changes as we iterate. Pin to an exact version, expect churn, and please file issues for friction or missing parity.

See rust/README.md for the full overview, examples, and the build/test commands. Generated types follow the same schema-driven flow used by the other SDKs (scripts/codegen/rust.ts).

CI for the new crate runs in .github/workflows/rust-sdk-tests.yml.

  Generated via Copilot (Claude Opus 4.7) on behalf of @tclem

tclem and others added 5 commits April 28, 2026 13:27
Adds the Copilot Rust SDK (`copilot-sdk` crate) under `rust/`,
alongside Rust codegen plumbed into `scripts/codegen/` and CI under
`.github/workflows/rust-sdk-tests.yml`. The crate ships a JSON-RPC
client, session lifecycle management, system message transforms,
permission policy helpers, the `define_tool` adapter, and per-event
`SessionHandler`/`SessionHooks` traits.

Includes:

- 14 ported E2E scenarios under `rust/tests/` driving the replay-proxy
  harness, plus a hand-curated set of unit tests.
- A rust-coding-skill (`.github/skills/rust-coding-skill/`) capturing
  conventions for error handling, async/concurrency, tracing, and the
  intentional trait exceptions in the SDK's public API.
- Release tooling: `rust-publish-release.yml`, `RELEASING.md`, and
  protocol-version generation wired into the existing automation.
- `PermissionResult` extended with `Deferred` and `Custom` variants
  for richer permission decisions.

Public API is held at 0.1.0-pre. Marked protocol-evolving public enums
`#[non_exhaustive]` so additive variants stay non-breaking.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Christopher Schleiden <cschleiden@github.com>
Co-authored-by: David Dossett <25163139+daviddossett@users.noreply.github.com>
Co-authored-by: Devraj Mehta <devm33@github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Evan Boyle <EvanBoyle@users.noreply.github.com>
Co-authored-by: Jeremy Moseley <jemoseley@microsoft.com>
Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com>
- **Broadcast subscriptions for lifecycle and session events.**
  `Client::subscribe_lifecycle()` and `Session::subscribe()` return
  `tokio::sync::broadcast::Receiver`; dropping the receiver
  unsubscribes. Replaces the prior callback-based `Client::on`,
  `Client::on_event_type`, `Session::on`, and `Unsubscribe` API.
  Spawned consumer tasks isolate panics naturally.
- **`PermissionResult` gains `Deferred` and `Custom` variants.**
  `Deferred` lets handlers resolve a request asynchronously via
  `session.permissions.handlePendingPermissionRequest` (notification
  path only — falls back to `Approved` on the direct RPC path).
  `Custom(Value)` lets handlers send arbitrary response payloads
  beyond the standard `approve-once` / `reject` shapes.
- **`#[non_exhaustive]` on protocol-evolving public enums**
  (`PermissionResult`, `SessionLifecycleEventType`,
  `GitHubReferenceType`, others) so additive variants stay
  non-breaking.
- **`ToolHandlerRouter` overrides per-event `SessionHandler` methods**
  so consumers can call `router.on_external_tool(...)` directly
  without unwrapping `HandlerResponse`.
- **`define_tool` accepts bare `async fn` items** in addition to
  closures, matching `tower::service_fn` /
  `hyper::service::service_fn` conventions. Documented in rustdoc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ypes

Generated code emitted `pub session_id: String` for every schema field named
`sessionId` and likewise for `requestId`, leaving consumers with mixed types:
`Session::id()` returned `SessionId` but `session.events_subscribe()` events
exposed `session_id: String`. Same papercut for request IDs in permission and
elicitation event payloads.

The newtypes are `#[serde(transparent)]` so the wire format is unchanged. This
adds a property-name override map to `scripts/codegen/rust.ts` that maps
`sessionId`, `remoteSessionId`, and `requestId` to the hand-authored types in
`crate::types`, and emits the matching `use` statement in both generated
modules. `mc_session_id` (MCP protocol metadata, not a Copilot session) stays
as `String`.

After regeneration: 27 fields converted to `SessionId` (including the handoff
event's `remoteSessionId`) and 25 to `RequestId`. The existing `PartialEq<str>`
/ `PartialEq<String>` impls on both newtypes mean test code like

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
define_tool's Fn(P) -> Fut bound gave closures only the deserialized
arguments, leaving session_id, tool_call_id, and tool_name unreachable.
That blocked the helper for any tool that needs to scope DB lookups to
a session, emit per-tool-call telemetry, or stream UI updates back to
the originating session — patterns that hit dozens of sites across
realistic tool suites.

Change the closure bound to Fn(ToolInvocation, P) -> Fut. The arguments
are moved out via mem::take before deserialization, so there is no
clone cost on the hot path. Closures that don't need the metadata
write |_inv, params|.

Also add ToolInvocation::params<P>() so long-form impl ToolHandler
blocks can deserialize without naming serde_json directly:

    async fn call(&self, inv: ToolInvocation) -> Result<ToolResult, Error> {
        let params: MyParams = inv.params()?;
        // …use inv.session_id alongside params…
    }

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Node, Python, and .NET all expose ping with an optional message.
Go requires it only because Go has no Option type — Rust has one,
so the API should match the languages with the same expressive power
rather than the one without.

Change ping(&self, message: &str) to ping(&self, message: Option<&str>).
When None, the message field is omitted from the request payload
rather than sent as an empty string.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 29, 2026 14:09
@tclem tclem requested a review from a team as a code owner April 29, 2026 14:09
@tclem tclem marked this pull request as draft April 29, 2026 14:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Rust SDK crate (copilot-sdk, library name copilot) to the monorepo, mirroring the existing SDKs’ JSON-RPC client/session model, and wires it into repo workflows and scenario coverage.

Changes:

  • Introduces the Rust SDK crate with protocol types, JSON-RPC transport, session/handler abstractions, examples, and tests.
  • Extends scenario samples and verification scripts to build/run Rust implementations alongside TS/Python/Go/C#.
  • Updates repo automation: codegen, just tasks, scenario-build CI, Rust SDK CI, and release/publish workflows.
Show a summary per file
File Description
test/scenarios/verify.sh Shows Rust status in the aggregated scenario runner UI.
test/scenarios/transport/tcp/verify.sh Builds/runs the Rust TCP transport scenario.
test/scenarios/transport/tcp/rust/Cargo.toml Rust TCP scenario crate manifest.
test/scenarios/transport/tcp/rust/src/main.rs Rust TCP sample using external host:port CLI server.
test/scenarios/transport/stdio/verify.sh Builds/runs the Rust stdio transport scenario.
test/scenarios/transport/stdio/rust/Cargo.toml Rust stdio scenario crate manifest.
test/scenarios/transport/stdio/rust/src/main.rs Rust stdio sample spawning the CLI child process.
test/scenarios/transport/stdio/README.md Documents Rust sample location/package name.
test/scenarios/tools/tool-overrides/verify.sh Builds/runs Rust tool override scenario.
test/scenarios/tools/tool-overrides/rust/Cargo.toml Rust tool-overrides scenario crate manifest (+derive).
test/scenarios/tools/tool-overrides/rust/src/main.rs Rust sample overriding built-in tool behavior.
test/scenarios/tools/tool-filtering/verify.sh Builds/runs Rust tool filtering scenario.
test/scenarios/tools/tool-filtering/rust/Cargo.toml Rust tool-filtering scenario crate manifest.
test/scenarios/tools/tool-filtering/rust/src/main.rs Rust sample limiting available tools.
test/scenarios/tools/skills/verify.sh Builds/runs Rust skills scenario.
test/scenarios/tools/skills/rust/Cargo.toml Rust skills scenario crate manifest.
test/scenarios/tools/skills/rust/src/main.rs Rust sample configuring skill directories + hooks.
test/scenarios/tools/no-tools/verify.sh Builds/runs Rust no-tools scenario.
test/scenarios/tools/no-tools/rust/Cargo.toml Rust no-tools scenario crate manifest.
test/scenarios/tools/no-tools/rust/src/main.rs Rust sample disabling tools + replacing system prompt.
test/scenarios/tools/mcp-servers/verify.sh Builds/runs Rust MCP servers scenario.
test/scenarios/tools/mcp-servers/rust/Cargo.toml Rust mcp-servers scenario crate manifest.
test/scenarios/tools/mcp-servers/rust/src/main.rs Rust sample passing MCP servers config to CLI.
test/scenarios/tools/custom-agents/verify.sh Builds/runs Rust custom agents scenario.
test/scenarios/tools/custom-agents/rust/Cargo.toml Rust custom-agents scenario crate manifest (+derive).
test/scenarios/tools/custom-agents/rust/src/main.rs Rust sample defining custom agents + custom tool.
test/scenarios/sessions/streaming/verify.sh Builds/runs Rust streaming scenario.
test/scenarios/sessions/streaming/rust/Cargo.toml Rust streaming scenario crate manifest.
test/scenarios/sessions/streaming/rust/src/main.rs Rust sample counting streaming delta events.
test/scenarios/sessions/session-resume/verify.sh Builds/runs Rust session resume scenario.
test/scenarios/sessions/session-resume/rust/Cargo.toml Rust session-resume scenario crate manifest.
test/scenarios/sessions/session-resume/rust/src/main.rs Rust sample creating + resuming session by ID.
test/scenarios/sessions/infinite-sessions/verify.sh Builds/runs Rust infinite sessions scenario.
test/scenarios/sessions/infinite-sessions/rust/Cargo.toml Rust infinite-sessions scenario crate manifest.
test/scenarios/sessions/infinite-sessions/rust/src/main.rs Rust sample exercising infinite session thresholds.
test/scenarios/sessions/concurrent-sessions/verify.sh Builds/runs Rust concurrent sessions scenario.
test/scenarios/sessions/concurrent-sessions/rust/Cargo.toml Rust concurrent-sessions scenario crate manifest.
test/scenarios/sessions/concurrent-sessions/rust/src/main.rs Rust sample running two sessions concurrently.
test/scenarios/prompts/system-message/verify.sh Builds/runs Rust system-message scenario.
test/scenarios/prompts/system-message/rust/Cargo.toml Rust system-message scenario crate manifest.
test/scenarios/prompts/system-message/rust/src/main.rs Rust sample replacing system message.
test/scenarios/prompts/reasoning-effort/verify.sh Builds/runs Rust reasoning-effort scenario.
test/scenarios/prompts/reasoning-effort/rust/Cargo.toml Rust reasoning-effort scenario crate manifest.
test/scenarios/prompts/reasoning-effort/rust/src/main.rs Rust sample setting reasoning_effort.
test/scenarios/modes/default/verify.sh Builds/runs Rust default mode scenario.
test/scenarios/modes/default/rust/Cargo.toml Rust default-mode scenario crate manifest.
test/scenarios/modes/default/rust/src/main.rs Rust sample using default tool-enabled mode.
test/scenarios/callbacks/user-input/verify.sh Builds/runs Rust user-input callback scenario.
test/scenarios/callbacks/user-input/rust/Cargo.toml Rust user-input scenario crate manifest.
test/scenarios/callbacks/user-input/rust/src/main.rs Rust sample handling ask_user prompts.
test/scenarios/callbacks/permissions/verify.sh Builds/runs Rust permission callback scenario.
test/scenarios/callbacks/permissions/rust/Cargo.toml Rust permissions scenario crate manifest.
test/scenarios/callbacks/permissions/rust/src/main.rs Rust sample logging/approving permissions.
test/scenarios/callbacks/hooks/verify.sh Builds/runs Rust hooks callback scenario.
test/scenarios/callbacks/hooks/rust/Cargo.toml Rust hooks scenario crate manifest.
test/scenarios/callbacks/hooks/rust/src/main.rs Rust sample implementing SessionHooks logging.
test/scenarios/RUST_COVERAGE.md Documents Rust scenario parity coverage/gaps.
scripts/codegen/package.json Adds Rust generation to codegen scripts.
nodejs/scripts/update-protocol-version.ts Generates Rust SDK protocol version constant.
justfile Adds Rust format/lint/test/codegen tasks.
.gitignore Ignores Rust scenario build artifacts/lockfiles.
.github/workflows/scenario-builds.yml Adds CI job building all Rust scenario crates.
.github/workflows/rust-sdk-tests.yml Adds Rust SDK CI (fmt/clippy/doc/test/semver-checks).
.github/workflows/rust-release-pr.yml Adds release-plz workflow to open Rust release PRs.
.github/workflows/rust-publish-release.yml Adds release-plz workflow to publish Rust crate.
.github/workflows/codegen-check.yml Ensures Rust codegen + protocol version regen in CI.
.github/skills/rust-coding-skill/SKILL.md Adds repo-specific Rust engineering guidance.
.github/skills/rust-coding-skill/examples.md Adds Rust SDK examples/patterns for contributors.
.github/copilot-instructions.md Updates repo guidance to include Rust SDK + Rust skill.
rust/Cargo.toml Defines the new copilot-sdk crate (features, deps, MSRV).
rust/Cargo.lock Locks Rust dependencies for deterministic builds.
rust/README.md Documents Rust SDK usage, architecture, and API surface.
rust/CHANGELOG.md Establishes initial changelog and release-plz plan.
rust/RELEASING.md Documents release/publish operations for maintainers.
rust/LICENSE Rust crate license file.
rust/rust-toolchain.toml Pins Rust toolchain version/components for the crate.
rust/release-plz.toml Configures release-plz behavior for the Rust crate.
rust/clippy.toml Configures Rust clippy rules (e.g., disallowed macros).
rust/.rustfmt.toml Stable rustfmt config (edition 2024).
rust/.rustfmt.nightly.toml Nightly rustfmt config enabling unstable formatting opts.
rust/.gitignore Ignores Rust target dir and backup lock files.
rust/build.rs Build-time CLI bundling/extraction codegen support.
rust/src/sdk_protocol_version.rs Generated SDK protocol version constant for Rust.
rust/src/generated/mod.rs Rust generated-type module root + re-exports.
rust/src/jsonrpc.rs Content-Length framed JSON-RPC transport implementation.
rust/src/router.rs Per-session routing of notifications/requests.
rust/src/handler.rs Session handler traits/events + default handlers.
rust/src/permission.rs Permission policy wrappers over SessionHandler.
rust/src/transforms.rs System message transform extension point + dispatcher.
rust/src/session.rs Session lifecycle/event loop plumbing (core runtime).
rust/tests/jsonrpc_test.rs Tests for JSON-RPC framing/routing (feature-gated).
rust/tests/protocol_version_test.rs Tests protocol version negotiation behavior.
rust/tests/integration_test.rs Ignored integration tests against real CLI.
rust/examples/chat.rs Interactive streaming chat example.
rust/examples/hooks.rs Hooks example for logging/auditing.
rust/examples/tool_server.rs Tool server example (feature-gated on derive).
rust/examples/lifecycle_observer.rs Observer example for lifecycle/session event streams.

Copilot's findings

  • Files reviewed: 102/107 changed files
  • Comments generated: 5

Comment thread rust/src/session.rs Outdated
Comment thread rust/build.rs
Comment thread rust/README.md Outdated
Comment thread test/scenarios/sessions/streaming/verify.sh
Comment thread .github/workflows/scenario-builds.yml
tclem and others added 5 commits April 29, 2026 07:24
cargo doc was running with --features test-support, which left the
derive feature off and made intra-doc links to define_tool and
schema_for resolve to nothing — failing under the crate's
deny(rustdoc::broken_intra_doc_links).

docs.rs already uses all-features (see Cargo.toml's
[package.metadata.docs.rs]); align CI with that so the docs job
matches what users will see on docs.rs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  emitted from the loop correlate to a session in traces. Matches
  the pattern documented in the rust-coding-skill.

- README.md / embeddedcli.rs: correct the embedded-CLI documentation
  to match what build.rs and embeddedcli.rs actually do — archives
  come from the github/copilot-cli GitHub Releases, integrity is
  SHA-256 against SHA256SUMS.txt, and the runtime cache path is
  ~/.cache/copilot-sdk-{version}/copilot.

- test/scenarios/sessions/streaming/verify.sh: drop a duplicate
  '# Go: build' comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Picks up the new model.call_failure session event (with its
ModelCallFailureData payload and ModelCallFailureSource enum) and
the new optional 'tip' field on session_info.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes path triggers and the regenerate step for other languages'
protocol-version files. Those drift checks are a pre-existing gap on
main and out of scope for the Rust SDK port.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tclem and others added 2 commits April 29, 2026 07:41
The 23-line setup checklist duplicated content already in
rust/RELEASING.md. One-line pointer is enough.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two scenarios still used the old `Fn(P) -> Fut` shape and broke when
the SDK switched to `Fn(ToolInvocation, P) -> Fut`. They don't use
the invocation field, so just bind it as `_inv`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1164 · ● 2.5M

Comment thread rust/src/session.rs Outdated
Cross-SDK consistency: every other SDK (Node, Python, Go, .NET) uses
`send`/`Send`/`SendAsync` plus `MessageOptions` as the public
parameter type. Rust was the outlier with `send_message` and
`SendOptions`, and the asymmetry with the existing `send_and_wait`
method made it read awkwardly.

- Rename `Session::send_message` -> `Session::send` (and the private
  helper `send_message_inner` -> `send_inner`).
- Rename the public `SendOptions` type -> `MessageOptions`.
- Delete the previous wire-level `MessageOptions` struct: it had no
  internal callers (the wire payload is hand-rolled in send_inner) and
  freeing the name was the cleanest path to parity.

Pre-1.0 type rename, no protocol or behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread .github/skills/rust-coding-skill/SKILL.md
Comment thread .github/skills/rust-coding-skill/SKILL.md
Comment thread .github/copilot-instructions.md Outdated
Comment thread rust/src/session.rs
Comment thread rust/src/lib.rs
Comment thread rust/src/types.rs Outdated
Comment thread rust/src/session.rs Outdated
Comment thread rust/src/types.rs
Comment thread rust/Cargo.toml Outdated
Comment thread rust/Cargo.toml Outdated
@stephentoub
Copy link
Copy Markdown
Collaborator

Thanks for getting this up.

It'd be helpful to run an agent over the other SDKs and this new one to look for any inconsistencies / gaps / divergences, so that we can then evaluate each and decide whether it's acceptable or should be addressed. The more consistent we can be across the SDKs, the easier it'll be to maintain them moving forward, the more information / docs about one will translate to consumption of the others, the better we'll be able to evolve with reduced concerns for how something we want to add may not fit well in a particular SDK, etc.

@github-actions

This comment has been minimized.

Previously Session::subscribe and Client::subscribe_lifecycle returned
raw tokio::sync::broadcast::Receiver<T> values. A survey of mature Rust
crates (tonic, lapin, rdkafka, redis-rs, tokio-tungstenite, iroh-gossip,
tokio-stream's BroadcastStream itself) found that none of them expose a
raw broadcast::Receiver in their public API; the dominant pattern is a
named newtype implementing futures::Stream, with overflow surfaced
explicitly in the item type.

Introduce a copilot::subscription module with:

  - EventSubscription / LifecycleSubscription newtypes
  - Inherent recv() returning Result<T, RecvError> for existing
    while-let loop ergonomics
  - Stream impl yielding Result<T, Lagged> so callers can use
    tokio_stream::StreamExt or futures::StreamExt combinators
  - Lagged / RecvError types owned by the SDK so consumers no longer
    import tokio's broadcast error types

Net effect: the channel choice is now an internal implementation detail.
We can swap broadcast for async-broadcast / flume / a custom backpressure
policy, or convert lag into an Event::Lagged variant, without a breaking
change to the public surface.

Existing while-let loops in tests and examples continue to compile and
behave identically: close and lag both exit the loop, matching
tokio::sync::broadcast::Receiver.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@tclem tclem marked this pull request as ready for review April 29, 2026 16:19
@tclem tclem marked this pull request as draft April 29, 2026 16:20
Local cargo +nightly fmt --check passed without `--config-path
.rustfmt.nightly.toml`, but CI runs with the explicit config and
flagged two diffs: import group flattening and test-mod import order.
Applied with the same flags CI uses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1164 · ● 2.2M

Comment thread rust/src/types.rs
…n pattern

Two changes prompted by review:

1. SKILL.md was too prose-heavy for an agent to scan. The "Idioms that
   don't port from other languages" section had grown to ~110 lines of
   essay-shaped advice with key rules buried in narrative. Replaced
   with three tight rule sections (Concurrency primitives, Optional
   fields and serde, plus a one-paragraph cross-language porting note
   that points back to the rule sections). Net: 345 -> 255 lines, no
   rule lost.

2. The trait-vs-callback-fields rule was implicit in the SessionHandler
   description but not explicit. Codified in "Traits and conversions"
   as a primary directive: prefer one trait with one default-impl
   method per event over per-event Box<dyn Fn> fields. Cited the three
   precedent traits in async Rust:

   - tower_lsp::LanguageServer (63 methods, default impls; LSP wire
     protocol)
   - rmcp::ServerHandler (26 methods, all default; MCP wire protocol)
   - notify::EventHandler (single on_event(enum) for uniform-shape
     events)

   Confirmed via research that no major async Rust crate ships
   per-event Box<dyn Fn> callback fields as its primary API; the
   pattern fights Send + Sync + 'static, fragments consumer state
   across closures, and skips exhaustiveness. The SDK's SessionHandler
   already uses the recommended shape (per-method with defaults plus a
   default on_event dispatcher); the doc just hadn't named the pattern.

Section ordering tightened: Traits/conversions now leads into
Extension points, then Concurrency primitives (channels matrix +
cancellation), then Optional fields/serde. Reads top-to-bottom for
an agent picking up a fresh task.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1164 · ● 1.9M

Comment thread rust/src/types.rs
/// Force-fail resume if the session does not exist on disk, instead of
/// silently starting a new session.
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_resume: Option<bool>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency gap: continue_pending_work missing from ResumeSessionConfig

All other SDKs expose a continuePendingWork / continue_pending_work / ContinuePendingWork field on their resume config, but the Rust ResumeSessionConfig doesn't include it:

SDK Field
Node.js continuePendingWork?: boolean
Python continue_pending_work: bool
Go ContinuePendingWork bool
.NET bool? ContinuePendingWork
Rust ❌ missing

This field is important for the "pending work resume" pattern — when a session is abandoned mid-tool-execution (e.g. via force_stop), setting continue_pending_work: true on resume instructs the CLI to replay and complete those pending tool/permission requests.

Suggested addition (after disable_resume):

/// When `true`, instructs the runtime to continue any pending tool calls
/// or permission requests that were outstanding when the session was
/// last disconnected. Use in combination with [`Client::force_stop`] to
/// hand off a live session to a new client without losing in-flight work.
/// See [`SessionConfig`] for the equivalent field on initial session creation.
#[serde(skip_serializing_if = "Option::is_none")]
pub continue_pending_work: Option<bool>,

And a corresponding builder method in impl ResumeSessionConfig:

pub fn with_continue_pending_work(mut self, value: bool) -> Self {
    self.continue_pending_work = Some(value);
    self
}

Don't forget to include it in ResumeSessionConfig::new (as None) and in the Debug impl.

Cross-SDK parity gap caught by the SDK consistency reviewer. All
four other SDKs (Node, Python, Go, .NET) expose this field; Rust
omitted it.

The field opts the runtime into continuing any tool calls or
permission requests that were pending when the previous connection
was dropped — it's the key enabler of the pending-work-handoff
pattern used together with `Client::force_stop` to migrate a session
from one process to another without losing in-flight work.

Plumbing:

- `pub continue_pending_work: Option<bool>` on `ResumeSessionConfig`
  with `#[serde(skip_serializing_if = "Option::is_none")]`. Container
  `rename_all = "camelCase"` covers the wire-name `continuePendingWork`.
  No manual payload-construction code is needed; resume_session
  serializes config -> wire via `serde_json::to_value(&config)`.
- `with_continue_pending_work(bool)` builder.
- Default impl: `None`.
- Debug impl: includes the field.

Tests:

- `resume_session_config_builder_composes` extended to cover the
  new field.
- `resume_session_config_serializes_continue_pending_work_to_camel_case`
  asserts the wire shape (`continuePendingWork: true`) and that
  unset values are omitted (skip_serializing_if).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1164 · ● 2.8M

Comment thread rust/README.md
`AutoModeSwitchResponse::{Yes, YesAlways, No}`. Default impl declines.
Cross-SDK parity is post-release follow-up — Node / Python / Go / .NET
consumers currently observe the request as a raw event and must drive
the wire response themselves.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Rust-only API" section documents on_auto_mode_switch as a Rust-only typed handler with a note that "Cross-SDK parity is post-release follow-up." The on_exit_plan_mode handler on SessionHandler is in the same situation — it's a typed Rust-only callback for exit_plan_mode.requested (other SDKs expose this only as a raw SessionEvent with no typed dispatch path), but it isn't mentioned here.

Worth adding a parallel bullet for on_exit_plan_mode so consumers have the same explicit heads-up that they would for on_auto_mode_switch:

- **`SessionHandler::on_exit_plan_mode`** — typed handler for the CLI's
  plan-mode exit prompt (`exit_plan_mode.requested`). Returns
  `ExitPlanModeResult` (default: approved with no action). Cross-SDK parity
  is post-release follow-up — Node / Python / Go / .NET consumers currently
  observe the request as a raw event and must drive the wire response
  themselves.

Minor documentation gap only — no code change needed.

Closes three cross-SDK parity gaps surfaced by the post-merge audit
against upstream commits 662f270, d3abfa2, and 180ca47.

ClientOptions::copilot_home: Option<PathBuf>
- Override the CLI's data directory. Exported as COPILOT_HOME to the
  spawned CLI process. Mirrors Node's copilotHome / Python's
  copilot_home.

ClientOptions::tcp_connection_token: Option<String>
- Optional auth token for TCP transport. Sent in the new `connect`
  JSON-RPC handshake (with backward-compat fall-back to `ping` for
  legacy CLI servers) and exported as COPILOT_CONNECTION_TOKEN to
  spawned CLI processes. When the SDK spawns its own CLI in TCP mode
  and this is unset, a UUID is generated so the loopback listener is
  safe by default. Combining with Transport::Stdio returns
  Error::InvalidConfig from Client::start. Mirrors Node's
  tcpConnectionToken / Python's tcp_connection_token / .NET's
  TcpConnectionToken.
- New `connect` handshake: verify_protocol_version now calls `connect`
  first, falling back to `ping` on -32601 MethodNotFound. The handshake
  carries the effective connection token. Matches Node's
  internalRpc.connect({ token }) sequence in client.ts.
- Adds `uuid` build-dep (1.x with v4 feature) for the auto-generated
  loopback token. Cryptographically-secure source via getrandom (already
  in the transitive dep tree).

SessionConfig::instruction_directories + ResumeSessionConfig::
instruction_directories: Option<Vec<PathBuf>>
- Additional directories the CLI searches for custom instruction files,
  distinct from skill_directories. Pure passthrough on the wire.
  Mirrors Node/Python instructionDirectories.

Error::InvalidConfig(String)
- New #[non_exhaustive] variant for client-construction errors that
  surface from Client::start (token + stdio, empty token, etc).

ClientInner gains effective_connection_token: Option<String> populated
in start(); from_transport now takes the token through (8 args ->
suppressed clippy::too_many_arguments on the internal helper).

Tests:
- protocol_version_test: existing tests updated for the new
  connect-then-ping fallback flow (server responds MethodNotFound on
  connect to exercise the legacy compat path).
- protocol_version_test::connect_handshake_supplies_protocol_version:
  new positive-path test — server responds to connect with a
  protocolVersion.
- lib::tests::build_command_sets_copilot_home_env_when_configured
- lib::tests::build_command_sets_connection_token_env_when_configured
- lib::tests::start_rejects_token_with_stdio_transport
- lib::tests::start_rejects_empty_connection_token

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

tclem and others added 3 commits May 4, 2026 07:45
…rage

Two related changes:

1. Drop the `uuid = "1"` dep added in 02780f0; replace with a
   `generate_connection_token()` helper that pulls 16 bytes from
   `getrandom::getrandom` and hex-encodes them. Same 128 bits of CSPRNG
   entropy, no semantic mismatch (the connection token is an opaque
   secret, not an identifier — typing it as a UUID would conflict with
   the existing pre-1.0 review consensus that schema-shaped IDs stay
   `String`). Output is a 32-char lowercase hex string, distinguishable
   from UUIDs at a glance.

   Rustdoc on the helper documents the decision so a future reader
   doesn't re-litigate it.

2. Address four reviewer items flagged on the github-app sync PR
   (#4399), all of which routed back upstream because src/ and tests/
   are mirrored from copilot-sdk:

   a. `session_config_serializes_instruction_directories_to_camel_case`
      — wire-shape test that `with_instruction_directories(...)`
      populates the field and serializes to `instructionDirectories`.

   b. `resume_session_config_serializes_instruction_directories_to_camel_case`
      — same on the resume path.

   c. `connect_handshake_forwards_explicit_token` and
      `connect_handshake_forwards_auto_generated_token` —
      positive coverage that `tcp_connection_token` actually reaches
      the outbound `connect` request's `token` param. Auto-generated
      case also asserts the shape (32-char lowercase hex) so a
      regression in the helper can't silently weaken loopback
      authentication.

   d. Rewrote the rustdoc on `Client::verify_protocol_version`. It
      claimed "sends a `ping` RPC", but the implementation now tries
      `connect` first and falls back to `ping` only on -32601. New
      docs describe the full handshake sequence and the token-
      forwarding semantics.

Test-support surface: adds two test-only entry points on `Client`
(`from_streams_with_connection_token`, `generate_connection_token_for_test`)
gated behind `cfg(any(test, feature = "test-support"))`, mirroring the
existing `from_streams_with_trace_provider` shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codegen was crashing on the post-1.0.41-0 schema with
`TypeError: s.split is not a function` at toPascalCase. Root cause:
the new `connect` JSON-RPC method's `ConnectResult.ok` field is
declared as `{ "type": "boolean", "enum": [true] }` (a single-value
boolean enum used as a "must-be-true" discriminant). The Rust
generator's `emitRustStringEnum` assumes string values, fed a
boolean, and panicked.

Python and Go generators already pre-process the schema with
`stripBooleanLiterals` (utils.ts:146) for exactly this reason —
upstream PR #1176 added the helper precisely because quicktype's
Python/Go renderers crashed on the same input. The TypeScript and
C# generators handle it natively. Rust's generator wasn't pre-
processing, so it inherited the same crash.

Fix: apply `stripBooleanLiterals` to both `apiSchema` and
`sessionEventsSchema` in `rust.ts` before postProcessSchema /
emit. Mirrors the Python/Go invocation pattern. Boolean
literal narrowing isn't expressible in Rust enums anyway —
the field stays `pub ok: bool`.

Regen produces two new types from the post-1.0.41-0 schema:
- `ConnectRequest` (token: Option<String>)
- `ConnectResult` (ok: bool, protocolVersion: u32)
- rpc_methods::CONNECT constant

These reflect the `connect` JSON-RPC method our hand-coded
verify_protocol_version handshake already invokes via
`client.call("connect", ...)`. We don't switch the call site to
the typed RPC because the existing untyped call matches how
`ping` is invoked next door, and the handshake needs the special
MethodNotFound fallback that doesn't fit the typed RPC abstraction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Now that codegen produces ConnectRequest / ConnectResult types
(b9b4d2b regen output), the hand-coded JSON-building in
Client::connect_handshake is just untyped boilerplate. Switches the
implementation to the typed `client.rpc().connect(ConnectRequest {
token })` call. Same wire shape; same MethodNotFound fallback at the
verify_protocol_version call site (lifted-out fallback, not
swallowed by the handshake helper); typed extraction of
protocol_version replaces the manual `value.get("protocolVersion")`
JSON walk.

ping() stays as a public hand-coded wrapper because the
`Option<&str>` ergonomics don't translate to the typed PingRequest
struct, and removing the public function would be a breaking API
change.

Test fixtures: extended the mock-server `connect` response stubs
in protocol_version_test.rs to include the now-required `version`
field on ConnectResult. Schema declares it required; deserialization
through the typed ConnectResult now enforces it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Pre-1.0 audit caught that `Client::send_telemetry` and
`Session::send_telemetry` were Rust-only public surface for an RPC
method (`sendTelemetry` / `session.sendTelemetry`) that:

- Is not described in `api.schema.json`.
- Is not exposed by the Node, Python, Go, or .NET SDKs (zero
  matches in their source trees).
- Has been moving around on the CLI side (the impl had a
  METHOD_NOT_FOUND fallback between top-level `sendTelemetry` and
  namespaced `server.sendTelemetry` to compensate).

A schema-undocumented + cross-SDK-absent public method is the
shape of an internal CLI knob, not a stable consumer API.
Consumers' canonical telemetry surface is the spawn-time env
injection via `ClientOptions::telemetry` (`TelemetryConfig`).
That stays.

Changes:
- Removed `Client::send_telemetry`, the cached
  `ServerTelemetryRpcMethod` enum, and the
  `ClientInner::server_telemetry_method` field that backed its
  fallback caching.
- Removed `Session::send_telemetry`.
- Removed `ServerTelemetryEvent` and `SessionTelemetryEvent`
  types from `types.rs`.
- Updated three integration tests in `tests/session_test.rs`
  (`send_telemetry_injects_payload_and_session_id`,
  `server_send_telemetry_sends_correct_payload`,
  `server_send_telemetry_falls_back_to_namespaced_method_and_caches_it`)
  by removing them, plus a fourth case in the dispatch-table
  test.
- Dropped now-unused local `METHOD_NOT_FOUND` const from
  the test file.
- Removed the "Rust-only API" README bullet describing the
  shortcut.
- Updated CHANGELOG to drop the type and method enumerations.

Net surface for telemetry: `ClientOptions::telemetry` +
`with_telemetry()` builder, plus the env-var injection in
`Client::build_command`. That's the documented, schema-described,
cross-SDK-consistent way to forward telemetry config to the CLI;
emitting individual telemetry events from consumer code is
intentionally out of scope.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Same audit shape as the send_telemetry removal: a sweep of public
methods that exist as Rust convenience wrappers on top of a typed
RPC, with no equivalent surface in the Node, Python, Go, or .NET
SDKs (zero matches across all four SDK source trees).

The methods are reachable cross-SDK through the typed `rpc()`
namespace — Rust callers simply switch from
`session.method()` to `session.rpc().<namespace>().<method>()`,
mirroring how Node/Python/Go/.NET consumers already drive these.

Removed from `Session`:
- get_model
- set_mode / get_mode
- set_name / get_name
- read_plan / update_plan / delete_plan
- list_workspace_files / read_workspace_file / create_workspace_file
- start_fleet
- set_approve_all_permissions
- call_rpc (generic forwarder; the typed rpc() namespace replaces it)

Removed from `Client`:
- get_quota (typed `client.rpc().account().get_quota()` already
  available everywhere including Rust)

Kept on Session: send, send_and_wait, abort, set_model, log,
disconnect/destroy, subscribe, capabilities, cancellation_token,
stop_event_loop, ui (and the field accessors id/cwd/workspace_path/
remote_url). These are either present in every SDK already (abort,
set_model, send/send_and_wait, log, disconnect) or are Rust-shape
helpers that don't have a typed RPC equivalent (subscribe,
cancellation_token, ui sub-API).

Updated:
- README "Rust-only API" section: dropped the "First-class Session
  convenience methods" bullet and the Client::get_quota bullet.
  What remains as Rust-only is now strictly language-shape items
  (newtypes, Transport enum, permission builders, from_streams,
  on_auto_mode_switch).
- CHANGELOG: dropped enumeration of the removed methods; restated
  what stays.
- 5 integration tests removed (get_name, set_name, list_workspace_files,
  read_workspace_file, create_workspace_file) plus the dispatch-table
  case for session.plan.delete.
- Unused imports cleaned up in session.rs.

Migration for the github-app consumer (the only known caller):
- session.set_approve_all_permissions(b)
  -> session.rpc().permissions().set_approve_all(
       PermissionsSetApproveAllRequest { enabled: b })
- session.set_mode("plan")
  -> session.rpc().mode().set(ModeSetRequest { mode: ... })
- session.read_plan()
  -> session.rpc().plan().read()
- (etc — all typed RPC namespace calls)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@stephentoub
Copy link
Copy Markdown
Collaborator

request_auto_mode_switch + on_auto_mode_switch handler —

Were these recently added in runtime? We haven't added anything for them yet in the other SDKs. Could we remove them from rust for now?

disabled_mcp_servers

Same here. This isn't exposed in any of the other SDKs.

If the desire ends up being to have this, we can add it across all of them, including Rust, in one fell swoop.

env_value_mode

Ditto. I'm not even sure what this is :)

Brought in 12 commits from origin/main, including CLI bumps to
1.0.41-0 and 1.0.41-1, plus upstream PR #966 ("Add provider model
and token limit overrides to ProviderConfig"). One trivial codegen
diff (single doc-comment update on `CustomAgentsUpdatedAgent.tools`
for the new "or null when all tools are available" semantics).

PR #966 added four new fields to ProviderConfig across all SDKs:
- `model_id: Option<String>` (well-known model ID for agent config
  + token limit lookup; falls back to SessionConfig::model)
- `wire_model: Option<String>` (model name sent to provider API for
  inference; falls back to model_id, then to SessionConfig::model)
- `max_prompt_tokens: Option<i64>` (overrides resolved model's
  default max prompt tokens; triggers compaction)
- `max_output_tokens: Option<i64>` (overrides resolved model's
  default max output tokens; truncates response)

Plus matching `with_*` builders. Wire-shape: camelCase
(`modelId`/`wireModel`/`maxPromptTokens`/`maxOutputTokens`),
skip_serializing_if when unset.

Extended `provider_config_builder_composes` test to exercise all
four fields and assert their wire shape (camelCase + omission).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Collaborator

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@stephentoub
Copy link
Copy Markdown
Collaborator

(We will follow-up in a subsequent PR addressing the remaining feedback.)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Cross-SDK Consistency Review — PR #1164 (Add Rust SDK)

This PR adds a brand-new Rust SDK. I reviewed it for cross-language API consistency with the Node.js, Python, Go, and .NET SDKs.

✅ Core API: well-aligned

The fundamental surface is consistent across all five SDKs:

Feature Node Python Go .NET Rust
create_session / resume_session
send / send_and_wait
get_messages, abort, set_model, log
disconnect / destroy
force_stop, continue_pending_work
ping, get_status, get_auth_status, list_models
list_sessions, get_session_metadata, delete_session
get_last_session_id, get/set_foreground_session_id
Hooks, transforms, tools, permissions, elicitation
Streaming, infinite sessions, telemetry, BYOK

The intentional divergences (typed newtypes, enum Transport, Client::from_streams, SessionFsProvider registration style, on_auto_mode_switch) are well-explained in the README's "Differences From Other SDKs" section. ✅

⚠️ One documentation bug found

The README's ### Session reference block (lines 107–124) documents several session methods as if they are direct convenience methods on Session, but they are not implemented on the struct — only session.rpc().X() equivalents exist:

  • session.get_model() — not implemented (use session.rpc().model().get_current())
  • session.get_mode() / session.set_mode() — not implemented (use session.rpc().mode().get()/set())
  • session.list_workspace_files() / session.read_workspace_file() — not implemented (use session.rpc().workspaces().*)
  • session.read_plan() / session.update_plan() — not implemented (use session.rpc().plan().*)
  • session.start_fleet() — not implemented (use session.rpc().fleet().start())

Since these are in rust,ignore blocks they aren't caught by cargo test --doc. An inline review comment has been left on the specific lines. The fix is either to implement the wrappers or update the code examples to use session.rpc().*.

This does not affect any other SDK — it's specific to the Rust README documentation.

Generated by SDK Consistency Review Agent for issue #1164 · ● 2.8M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1164 · ● 2.8M

Comment thread rust/README.md
Comment on lines +107 to +127
// Model management
let model = session.get_model().await?;
session.set_model("claude-sonnet-4.5", None).await?;
// Mode management (interactive, plan, autopilot)
let mode = session.get_mode().await?;
session.set_mode("autopilot").await?;
// Workspace files
let files = session.list_workspace_files().await?;
let content = session.read_workspace_file("plan.md").await?;
// Plan management
let (exists, content) = session.read_plan().await?;
session.update_plan("Updated plan content").await?;
// Fleet (sub-agents)
session.start_fleet(Some("Implement the auth module")).await?;
// Cleanup (preserves on-disk session state for later resume)
session.disconnect().await?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation bug: several session.X() calls shown here don't exist on Session

Lines 108–124 document the following as direct convenience methods on Session, but none of them are actually implemented on the struct (only set_model and get_messages / abort are):

Shown in README Actual API
session.get_model() session.rpc().model().get_current().await?
session.get_mode() session.rpc().mode().get().await?
session.set_mode("autopilot") session.rpc().mode().set(ModeSetRequest { mode: "autopilot".into() }).await?
session.list_workspace_files() session.rpc().workspaces().list_files().await?
session.read_workspace_file("plan.md") session.rpc().workspaces().read_file(WorkspacesReadFileRequest { path: "plan.md".into() }).await?
session.read_plan() session.rpc().plan().read().await?
session.update_plan("...") session.rpc().plan().update(PlanUpdateRequest { ... }).await?
session.start_fleet(Some("...")) session.rpc().fleet().start(FleetStartRequest { ... }).await?

Because the code block uses rust,ignore, this isn't caught by cargo test --doc. A user copying these snippets from the README will get a compile error like no method named 'get_model' found for struct 'Session'.

Suggestion: Either implement these as ergonomic wrappers on Session (thin delegations to rpc()), or update this section to use session.rpc().* calls and add a note that the typed RPC namespace is the primary access point for these operations. If the wrappers are planned as a follow-up, it would be worth adding a // TODO: convenience wrapper note and linking to the "Typed RPC namespace" section so readers know the right path today.

@stephentoub stephentoub added this pull request to the merge queue May 6, 2026
Merged via the queue into main with commit 19b4ad5 May 6, 2026
39 checks passed
@stephentoub stephentoub deleted the tclem/rust-sdk-release-prep branch May 6, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants